/*
 *
 *  Copyright 2016 Robert Winkler
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *
 */
package io.github.resilience4j.circuitbreaker;

import io.github.resilience4j.core.lang.Nullable;
import io.github.resilience4j.core.predicate.PredicateCreator;

import java.time.Duration;
import java.util.function.Predicate;


/**
 * A {@link CircuitBreakerConfig} configures a {@link CircuitBreaker}
 */
public class CircuitBreakerConfig {

    public static final int DEFAULT_FAILURE_RATE_THRESHOLD = 50; // Percentage
    public static final int DEFAULT_SLOW_CALL_RATE_THRESHOLD = 100; // Percentage
    public static final int DEFAULT_WAIT_DURATION_IN_OPEN_STATE = 60; // Seconds
    public static final int DEFAULT_PERMITTED_CALLS_IN_HALF_OPEN_STATE = 10;
    public static final int DEFAULT_MINIMUM_NUMBER_OF_CALLS = 100;
    public static final int DEFAULT_SLIDING_WINDOW_SIZE = 100;
    public static final int DEFAULT_SLOW_CALL_DURATION_THRESHOLD = 60; // Seconds
    private static final Predicate<Throwable> DEFAULT_RECORD_FAILURE_PREDICATE = throwable -> true;
    public static final SlidingWindow DEFAULT_SLIDING_WINDOW_TYPE = SlidingWindow.COUNT_BASED;

    @SuppressWarnings("unchecked")
    private Class<? extends Throwable>[] recordExceptions = new Class[0];
    @SuppressWarnings("unchecked")
    private Class<? extends Throwable>[] ignoreExceptions = new Class[0];

    private float failureRateThreshold = DEFAULT_FAILURE_RATE_THRESHOLD;
    private int permittedNumberOfCallsInHalfOpenState = DEFAULT_PERMITTED_CALLS_IN_HALF_OPEN_STATE;
    private int slidingWindowSize = DEFAULT_SLIDING_WINDOW_SIZE;
    private SlidingWindow slidingWindowType = DEFAULT_SLIDING_WINDOW_TYPE;
    private int minimumNumberOfCalls = DEFAULT_MINIMUM_NUMBER_OF_CALLS;
    private Duration waitDurationInOpenState = Duration.ofSeconds(DEFAULT_WAIT_DURATION_IN_OPEN_STATE);
    // The default exception predicate counts all exceptions as failures.
    private Predicate<Throwable> recordFailurePredicate = DEFAULT_RECORD_FAILURE_PREDICATE;
    private boolean automaticTransitionFromOpenToHalfOpenEnabled = false;
    private float slowCallRateThreshold = DEFAULT_SLOW_CALL_RATE_THRESHOLD;
    private Duration slowCallDurationThreshold = Duration.ofSeconds(DEFAULT_SLOW_CALL_DURATION_THRESHOLD);


    private CircuitBreakerConfig() {
    }

    /**
     * Returns a builder to create a custom CircuitBreakerConfig.
     *
     * @return a {@link Builder}
     */
    public static Builder custom() {
        return new Builder();
    }

    /**
     * Returns a builder to create a custom CircuitBreakerConfig based on another CircuitBreakerConfig.
     *
     * @return a {@link Builder}
     */
    public static Builder from(CircuitBreakerConfig baseConfig) {
        return new Builder(baseConfig);
    }

    /**
     * Creates a default CircuitBreaker configuration.
     *
     * @return a default CircuitBreaker configuration.
     */
    public static CircuitBreakerConfig ofDefaults() {
        return new Builder().build();
    }

    public float getFailureRateThreshold() {
        return failureRateThreshold;
    }

    public Duration getWaitDurationInOpenState() {
        return waitDurationInOpenState;
    }

    public int getSlidingWindowSize() {
        return slidingWindowSize;
    }

    public Predicate<Throwable> getRecordFailurePredicate() {
        return recordFailurePredicate;
    }

    public boolean isAutomaticTransitionFromOpenToHalfOpenEnabled() {
        return automaticTransitionFromOpenToHalfOpenEnabled;
    }

    public int getMinimumNumberOfCalls() {
        return minimumNumberOfCalls;
    }

    public int getPermittedNumberOfCallsInHalfOpenState() {
        return permittedNumberOfCallsInHalfOpenState;
    }

    public SlidingWindow getSlidingWindowType() {
        return slidingWindowType;
    }

    public float getSlowCallRateThreshold() {
        return slowCallRateThreshold;
    }

    public Duration getSlowCallDurationThreshold() {
        return slowCallDurationThreshold;
    }

    public static class Builder {

        @Nullable
        private Predicate<Throwable> recordFailurePredicate;
        @SuppressWarnings("unchecked")
        private Class<? extends Throwable>[] recordExceptions = new Class[0];
        @SuppressWarnings("unchecked")
        private Class<? extends Throwable>[] ignoreExceptions = new Class[0];
        private float failureRateThreshold = DEFAULT_FAILURE_RATE_THRESHOLD;
        private int minimumNumberOfCalls = DEFAULT_MINIMUM_NUMBER_OF_CALLS;
        private int permittedNumberOfCallsInHalfOpenState = DEFAULT_PERMITTED_CALLS_IN_HALF_OPEN_STATE;
        private int slidingWindowSize = DEFAULT_SLIDING_WINDOW_SIZE;
        private Duration waitDurationInOpenState = Duration.ofSeconds(DEFAULT_SLOW_CALL_DURATION_THRESHOLD);
        private boolean automaticTransitionFromOpenToHalfOpenEnabled = false;
        private SlidingWindow slidingWindowType = DEFAULT_SLIDING_WINDOW_TYPE;
        private float slowCallRateThreshold = DEFAULT_SLOW_CALL_RATE_THRESHOLD;
        private Duration slowCallDurationThreshold = Duration.ofSeconds(DEFAULT_SLOW_CALL_DURATION_THRESHOLD);

        public Builder(CircuitBreakerConfig baseConfig) {
            this.waitDurationInOpenState = baseConfig.waitDurationInOpenState;
            this.permittedNumberOfCallsInHalfOpenState = baseConfig.permittedNumberOfCallsInHalfOpenState;
            this.slidingWindowSize = baseConfig.slidingWindowSize;
            this.slidingWindowType = baseConfig.slidingWindowType;
            this.minimumNumberOfCalls = baseConfig.minimumNumberOfCalls;
            this.failureRateThreshold = baseConfig.failureRateThreshold;
            this.ignoreExceptions = baseConfig.ignoreExceptions;
            this.recordExceptions = baseConfig.recordExceptions;
            this.recordFailurePredicate = baseConfig.recordFailurePredicate;
            this.automaticTransitionFromOpenToHalfOpenEnabled = baseConfig.automaticTransitionFromOpenToHalfOpenEnabled;
            this.slowCallRateThreshold = baseConfig.slowCallRateThreshold;
            this.slowCallDurationThreshold = baseConfig.slowCallDurationThreshold;
        }

        public Builder() {

        }

        /**
         * Configures the failure rate threshold in percentage.
         * If the failure rate is equal or greater than the threshold the CircuitBreaker transitions to open and starts short-circuiting calls.
         * <p>
         * The threshold must be greater than 0 and not greater than 100. Default value is 50 percentage.
         *
         * @param failureRateThreshold the failure rate threshold in percentage
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder failureRateThreshold(float failureRateThreshold) {
            if (failureRateThreshold <= 0 || failureRateThreshold > 100) {
                throw new IllegalArgumentException("failureRateThreshold must be between 1 and 100");
            }
            this.failureRateThreshold = failureRateThreshold;
            return this;
        }

        /**
         * Configures a threshold in percentage. The CircuitBreaker considers a call as slow when the call duration is greater than {@link #slowCallDurationThreshold(Duration)}.
         * When the percentage of slow calls is equal or greater the threshold, the CircuitBreaker transitions to open and starts short-circuiting calls.
         *
         * <p>
         * The threshold must be greater than 0 and not greater than 100.
         * Default value is 100 percentage which means that all recorded calls must be slower than {@link #slowCallDurationThreshold(Duration)}.
         *
         * @param slowCallRateThreshold the slow calls threshold in percentage
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder slowCallRateThreshold(float slowCallRateThreshold) {
            if (slowCallRateThreshold <= 0 || slowCallRateThreshold > 100) {
                throw new IllegalArgumentException("slowCallRateThreshold must be between 1 and 100");
            }
            this.slowCallRateThreshold = slowCallRateThreshold;
            return this;
        }

        /**
         * Configures the wait duration which specifies how long the CircuitBreaker should stay open, before it switches to half open.
         * Default value is 60 seconds.
         *
         * @param waitDurationInOpenState the wait duration which specifies how long the CircuitBreaker should stay open
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder waitDurationInOpenState(Duration waitDurationInOpenState) {
            if (waitDurationInOpenState.toMillis() < 1) {
                throw new IllegalArgumentException("waitDurationInOpenState must be at least 1[ms]");
            }
            this.waitDurationInOpenState = waitDurationInOpenState;
            return this;
        }

        /**
         * Configures the duration threshold above which calls are considered as slow and increase the slow calls percentage.
         * Default value is 60 seconds.
         *
         * @param slowCallDurationThreshold the duration above which calls are considered as slow
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder slowCallDurationThreshold(Duration slowCallDurationThreshold) {
            if (slowCallDurationThreshold.toNanos() < 1) {
                throw new IllegalArgumentException("slowCallDurationThreshold must be at least 1[ns]");
            }
            this.slowCallDurationThreshold = slowCallDurationThreshold;
            return this;
        }

        /**
         * Configures the number of permitted calls when the CircuitBreaker is half open.
         * <p>
         * The size must be greater than 0. Default size is 10.
         *
         * @param permittedNumberOfCallsInHalfOpenState the permitted number of calls when the CircuitBreaker is half open
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder permittedNumberOfCallsInHalfOpenState(int permittedNumberOfCallsInHalfOpenState) {
            if (permittedNumberOfCallsInHalfOpenState < 1) {
                throw new IllegalArgumentException("permittedNumberOfCallsInHalfOpenState must be greater than 0");
            }
            this.permittedNumberOfCallsInHalfOpenState = permittedNumberOfCallsInHalfOpenState;
            return this;
        }

        /**
         * @deprecated Use {@link #permittedNumberOfCallsInHalfOpenState(int)} instead.
         */
        public Builder ringBufferSizeInHalfOpenState(int ringBufferSizeInHalfOpenState) {
            if (ringBufferSizeInHalfOpenState < 1) {
                throw new IllegalArgumentException("ringBufferSizeInHalfOpenState must be greater than 0");
            }
            this.permittedNumberOfCallsInHalfOpenState = ringBufferSizeInHalfOpenState;
            return this;
        }

        /**
         * @deprecated Use {@link #slidingWindow(int, int, SlidingWindow)} instead.
         */
        public Builder ringBufferSizeInClosedState(int ringBufferSizeInClosedState) {
            if (ringBufferSizeInClosedState < 1) {
                throw new IllegalArgumentException("ringBufferSizeInClosedState must be greater than 0");
            }
            return slidingWindow(ringBufferSizeInClosedState, ringBufferSizeInClosedState, SlidingWindow.COUNT_BASED);
        }

        /**
         * Configures the sliding window which is used to record the outcome of calls when the CircuitBreaker is closed.
         * {@code slidingWindowSize} configures the size of the sliding window. Sliding window can either be count-based or time-based.
         * {@code minimumNumberOfCalls} configures the minimum number of calls which are required (per sliding window period) before the CircuitBreaker can calculate the error rate.
         * For example, if {@code minimumNumberOfCalls} is 10, then at least 10 calls must be recorded, before the failure rate can be calculated.
         * If only 9 calls have been recorded the CircuitBreaker will not transition to open even if all 9 calls have failed.
         *
         * If {@code slidingWindowSize} is 100 and {@code slidingWindowType} is COUNT_BASED, the last 100 calls are recorded and aggregated.
         * If {@code slidingWindowSize} is 10 and {@code slidingWindowType} is TIME_BASED, the calls of the last 10 seconds are recorded and aggregated.
         * <p>
         * The {@code slidingWindowSize} must be greater than 0.
         * The {@code minimumNumberOfCalls} must be greater than 0.
         * If the slidingWindowType is COUNT_BASED, the {@code minimumNumberOfCalls} cannot be greater than {@code slidingWindowSize}.
         * If the slidingWindowType is TIME_BASED, you can pick whatever you want.
         *
         * Default slidingWindowSize is 100, minimumNumberOfCalls is 100 and slidingWindowType is COUNT_BASED.
         *
         * @param slidingWindowSize the size of the sliding window when the CircuitBreaker is closed.
         * @param minimumNumberOfCalls the minimum number of calls that must be recorded before the failure rate can be calculated.
         * @param slidingWindowType the type of the sliding window. Either COUNT_BASED or TIME_BASED.
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder slidingWindow(int slidingWindowSize, int minimumNumberOfCalls, SlidingWindow slidingWindowType) {
            if (slidingWindowSize < 1) {
                throw new IllegalArgumentException("slidingWindowSize must be greater than 0");
            }
            if (minimumNumberOfCalls < 1) {
                throw new IllegalArgumentException("minimumNumberOfCalls must be greater than 0");
            }
            if (slidingWindowType == SlidingWindow.COUNT_BASED) {
                this.minimumNumberOfCalls = Math.min(minimumNumberOfCalls, slidingWindowSize);
            }else{
                this.minimumNumberOfCalls = minimumNumberOfCalls;
            }
            this.slidingWindowSize = slidingWindowSize;
            this.slidingWindowType = slidingWindowType;
            return this;
        }

        /**
         * Configures the size of the sliding window which is used to record the outcome of calls when the CircuitBreaker is closed.
         * {@code slidingWindowSize} configures the size of the sliding window. Sliding window can either be count-based or time-based.
         *
         * If {@code slidingWindowType} is COUNT_BASED, the last {@code slidingWindowSize} calls are recorded and aggregated.
         * If {@code slidingWindowType} is TIME_BASED, the calls of the last {@code slidingWindowSize} seconds are recorded and aggregated.
         * <p>
         * The {@code slidingWindowSize} must be greater than 0.
         * The {@code minimumNumberOfCalls} must be greater than 0.
         * If the slidingWindowType is COUNT_BASED, the {@code minimumNumberOfCalls} cannot be greater than {@code slidingWindowSize}.
         * If the slidingWindowType is TIME_BASED, you can pick whatever you want.
         *
         * Default slidingWindowSize is 100.
         *
         * @param slidingWindowSize the size of the sliding window when the CircuitBreaker is closed.
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder slidingWindowSize(int slidingWindowSize) {
            if (slidingWindowSize < 1) {
                throw new IllegalArgumentException("slidingWindowSize must be greater than 0");
            }
            this.slidingWindowSize = slidingWindowSize;
            return this;
        }

        /**
         * Configures configures the minimum number of calls which are required (per sliding window period) before the CircuitBreaker can calculate the error rate.
         * For example, if {@code minimumNumberOfCalls} is 10, then at least 10 calls must be recorded, before the failure rate can be calculated.
         * If only 9 calls have been recorded the CircuitBreaker will not transition to open even if all 9 calls have failed.
         *
         * Default minimumNumberOfCalls is 100
         *
         * @param minimumNumberOfCalls the minimum number of calls that must be recorded before the failure rate can be calculated.
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder minimumNumberOfCalls(int minimumNumberOfCalls) {
            if (minimumNumberOfCalls < 1) {
                throw new IllegalArgumentException("minimumNumberOfCalls must be greater than 0");
            }
            this.minimumNumberOfCalls = minimumNumberOfCalls;
            return this;
        }

        /**
         * Configures the type of the sliding window which is used to record the outcome of calls when the CircuitBreaker is closed.
         * Sliding window can either be count-based or time-based.
         *
         * If {@code slidingWindowType} is COUNT_BASED, the last {@code slidingWindowSize} calls are recorded and aggregated.
         * If {@code slidingWindowType} is TIME_BASED, the calls of the last {@code slidingWindowSize} seconds are recorded and aggregated.
         *
         * Default slidingWindowType is COUNT_BASED.
         *
         * @param slidingWindowType the type of the sliding window. Either COUNT_BASED or TIME_BASED.
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder slidingWindowType(SlidingWindow slidingWindowType) {
            this.slidingWindowType = slidingWindowType;
            return this;
        }

        /**
         * Configures a Predicate which evaluates if an exception should be recorded as a failure and thus increase the failure rate.
         * The Predicate must return true if the exception should count as a failure. The Predicate must return false, if the exception
         * should neither count as a failure nor success.
         *
         * @param predicate the Predicate which evaluates if an exception should count as a failure
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder recordFailure(Predicate<Throwable> predicate) {
            this.recordFailurePredicate = predicate;
            return this;
        }

        /**
         * Configures a list of error classes that are recorded as a failure and thus increase the failure rate.
         * Any exception matching or inheriting from one of the list should count as a failure, unless ignored via
         *
         * @param errorClasses the error classes that are recorded
         * @return the CircuitBreakerConfig.Builder
         * @see #ignoreExceptions(Class[]) ). Ignoring an exception has priority over recording an exception.
         * <p>
         * Example:
         * recordExceptions(Throwable.class) and ignoreExceptions(RuntimeException.class)
         * would capture all Errors and checked Exceptions, and ignore unchecked
         * <p>
         * For a more sophisticated exception management use the
         * @see #recordFailure(Predicate) method
         */
        @SuppressWarnings("unchecked")
        @SafeVarargs
        public final Builder recordExceptions(@Nullable Class<? extends Throwable>... errorClasses) {
            this.recordExceptions = errorClasses != null ? errorClasses : new Class[0];
            return this;
        }

        /**
         * Configures a list of error classes that are ignored and thus neither count as a failure nor success.
         * Any exception matching or inheriting from one of the list will not count as a failure nor success, even if marked via
         *
         * @param errorClasses the error classes that are ignored
         * @return the CircuitBreakerConfig.Builder
         * @see #recordExceptions(Class[]) . Ignoring an exception has priority over recording an exception.
         * <p>
         * Example:
         * ignoreExceptions(Throwable.class) and recordExceptions(Exception.class)
         * would capture nothing
         * <p>
         * Example:
         * ignoreExceptions(Exception.class) and recordExceptions(Throwable.class)
         * would capture Errors
         * <p>
         * For a more sophisticated exception management use the
         * @see #recordFailure(Predicate) method
         */
        @SuppressWarnings("unchecked")
        @SafeVarargs
        public final Builder ignoreExceptions(@Nullable Class<? extends Throwable>... errorClasses) {
            this.ignoreExceptions = errorClasses != null ? errorClasses : new Class[0];
            return this;
        }

        /**
         * Enables automatic transition from OPEN to HALF_OPEN state once the waitDurationInOpenState has passed.
         *
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder enableAutomaticTransitionFromOpenToHalfOpen() {
            this.automaticTransitionFromOpenToHalfOpenEnabled = true;
            return this;
        }

        /**
         * Enables automatic transition from OPEN to HALF_OPEN state once the waitDurationInOpenState has passed.
         *
         * @return the CircuitBreakerConfig.Builder
         */
        public Builder automaticTransitionFromOpenToHalfOpenEnabled(boolean enableAutomaticTransitionFromOpenToHalfOpen) {
            this.automaticTransitionFromOpenToHalfOpenEnabled = enableAutomaticTransitionFromOpenToHalfOpen;
            return this;
        }

        /**
         * Builds a CircuitBreakerConfig
         *
         * @return the CircuitBreakerConfig
         */
        public CircuitBreakerConfig build() {
            CircuitBreakerConfig config = new CircuitBreakerConfig();
            config.waitDurationInOpenState = waitDurationInOpenState;
            config.slidingWindowType = slidingWindowType;
            config.slowCallDurationThreshold = slowCallDurationThreshold;
            config.slowCallRateThreshold = slowCallRateThreshold;
            config.failureRateThreshold = failureRateThreshold;
            config.slidingWindowSize = slidingWindowSize;
            config.minimumNumberOfCalls = minimumNumberOfCalls;
            config.permittedNumberOfCallsInHalfOpenState = permittedNumberOfCallsInHalfOpenState;
            config.recordExceptions = recordExceptions;
            config.ignoreExceptions = ignoreExceptions;
            config.automaticTransitionFromOpenToHalfOpenEnabled = automaticTransitionFromOpenToHalfOpenEnabled;
            config.recordFailurePredicate = createRecordFailurePredicate();
            return config;
        }

        private Predicate<Throwable> createRecordFailurePredicate() {
            return createRecordExceptionPredicate()
                    .and(PredicateCreator.createIgnoreExceptionsPredicate(ignoreExceptions)
                            .orElse(DEFAULT_RECORD_FAILURE_PREDICATE));
        }

        private Predicate<Throwable> createRecordExceptionPredicate() {
            return PredicateCreator.createRecordExceptionsPredicate(recordExceptions)
                    .map(predicate -> recordFailurePredicate != null ? predicate.or(recordFailurePredicate) : predicate)
                    .orElseGet(() -> recordFailurePredicate != null ? recordFailurePredicate : DEFAULT_RECORD_FAILURE_PREDICATE);
        }


    }

    public enum SlidingWindow {
        TIME_BASED, COUNT_BASED
    }
}
